package org.apache.ocean.solr.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import net.sourceforge.jsorter.ReflectColumns;
import net.sourceforge.jsorter.SortReflect;
import net.sourceforge.jsorter.Sorter;
import net.sourceforge.jsorter.SorterConstants;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.index.IndexCommitPoint;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.NativeFSLockFactory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.ocean.main.Batch.SlaveBatch;
import org.apache.ocean.main.Index.IndexSnapshot;
import org.apache.ocean.main.fields.SchemaField;
import org.apache.ocean.main.search.cache.Cache;
import org.apache.ocean.main.search.cache.CacheKey;
import org.apache.ocean.main.search.cache.FieldCacheKey;
import org.apache.ocean.main.search.cache.FieldIndexDatabase;
import org.apache.ocean.main.search.cache.FieldCacheKey.FieldIndex;
import org.apache.ocean.main.search.cache.FieldCacheKey.ValueIndexCursor;
import org.apache.ocean.main.util.CElement;
import org.apache.ocean.main.util.Constants;
import org.apache.ocean.main.util.Range;
import org.apache.ocean.main.util.XMLUtil;
import org.apache.ocean.util.ConcurrentTreeMap;
import org.jdom.Element;

public class DiskIndex extends DirectoryIndex {
	public static Logger log = Logger.getLogger(DiskIndex.class.getName());
	private final FSDirectory fsDirectory;
	private final File directoryFile;
	private IndexInfo indexInfo;

	// load existing index
	public DiskIndex(Long id, File directoryFile, TransactionSystem.Category category, Long snapshotId, IndexSchema.Category schemaCategory) throws Exception, IndexException, IOException {
		super(id, schemaCategory.getAnalyzer(), category);
		this.directoryFile = directoryFile;
		File writingIndexFile = new File(directoryFile, "writing.index");
		if (writingIndexFile.exists()) {
			throw new IndexNeverCompletedCopyException("index never completed copying");
		}
		File largeBatchStartedFile = new File(directoryFile, "largebatch.started");
		if (largeBatchStartedFile.exists()) {
			FileUtils.deleteDirectory(directoryFile);
			throw new IndexException("large batch never finished, directory: " + directoryFile.getAbsolutePath() + " deleted");
		}
		fsDirectory = FSDirectory.getDirectory(directoryFile);
		fsDirectory.setLockFactory(new NativeFSLockFactory(directoryFile));
		initialIndexReader = IndexReader.open(fsDirectory, indexDeletionPolicy);
		SnapshotReaderInfo snapshotReaderInfo = loadSnapshotReaderInfo(snapshotId);// loadHighestSnapshotReaderTuple();
		if (snapshotReaderInfo == null) {
			throw new IndexNeverCompletedCopyException("no snapshotReaderTuples available");
		}
		indexInfo = loadIndexInfo();
		createNewSnapshot(snapshotId, initialIndexReader, schemaCategory);
		cache = initCache();
	}

	// merge indices
	public DiskIndex(Long id, File directoryFile, List<? extends IndexSnapshot> indexSnapshots, TransactionSystem.Category category, IndexSchema.Category schemaCategory) throws Exception, IOException {
		super(id, schemaCategory.getAnalyzer(), category);
		this.directoryFile = directoryFile;
		File writingIndexFile = new File(directoryFile, "writing.index");
		FileUtils.touch(writingIndexFile);
		fsDirectory = FSDirectory.getDirectory(directoryFile);
		fsDirectory.setLockFactory(new NativeFSLockFactory(directoryFile));
		IndexReader[] indexReaders = getIndexReaders(indexSnapshots);
		RAMDirectory ramDirectory = new RAMDirectory();
		IndexWriter indexWriter = new IndexWriter(ramDirectory, false, defaultAnalyzer, true);
		indexWriter.setMergeScheduler(new SerialMergeScheduler());
		indexWriter.setUseCompoundFile(true);
		indexWriter.addIndexes(indexReaders);
		indexWriter.close();
		Directory.copy(ramDirectory, fsDirectory, true);
		
		Long highestSnapshotId = getHighestSnapshotId(indexSnapshots);
		Long highestDocumentId = getHighestDocumentId(indexSnapshots);
		
		indexInfo = new IndexInfo();
		indexInfo.setHighestDocumentID(highestDocumentId);
		indexInfo.setHighestSnapshotID(highestSnapshotId);
		saveIndexInfo(indexInfo);
		writingIndexFile.delete();
		initialIndexReader = IndexReader.open(fsDirectory, indexDeletionPolicy);
		cache = initCache();
		Set<CacheKey> cacheKeys = getCacheKeys(indexSnapshots);
		MergedDocMap mergedDocMap = new MergedDocMap(indexSnapshots);
		cache.merge(cacheKeys, mergedDocMap);
		for (FieldIndex fieldIndex : cache.getFieldIndices()) {
			writeFieldIndex(fieldIndex);
		}
	}
  
	public void close() {
		
	}
	
	private File getBtreeDirectory() {
		File btreeDirectory = new File(directoryFile, "btree");
		if (!btreeDirectory.exists())
			btreeDirectory.mkdirs();
		return btreeDirectory;
	}

	public Directory getDirectory() {
		return fsDirectory;
	}

	private File getIndexInfoFile() {
		return new File(directoryFile, "indexinfo.xml");
	}

	private IndexInfo loadIndexInfo() throws Exception {
		File file = getIndexInfoFile();
		Element element = XMLUtil.parseElement(file);
		return new IndexInfo(element);
	}

	private void saveIndexInfo(IndexInfo indexInfo) throws Exception {
		Element element = indexInfo.toElement();
		File file = getIndexInfoFile();
		XMLUtil.saveXML(element, file);
	}

	public static class IndexInfo implements CElement {
		private Long highestSnapshotID;
		private Long highestDocumentID;

		public IndexInfo() {
		}

		public Long getHighestSnapshotID() {
			return highestSnapshotID;
		}

		public void setHighestSnapshotID(Long highestSnapshotID) {
			this.highestSnapshotID = highestSnapshotID;
		}

		public Long getHighestDocumentID() {
			return highestDocumentID;
		}

		public void setHighestDocumentID(Long highestDocumentID) {
			this.highestDocumentID = highestDocumentID;
		}

		public IndexInfo(Element element) throws Exception {
			XMLUtil.reflectToObject(this, element);
		}

		public Element toElement() throws Exception {
			return XMLUtil.reflectToElement("indexinfo", this);
		}
	}

	public File getFile(String name) {
		return new File(directoryFile, name);
	}

	public boolean hasTooManyDeletedDocs(double percent) {
		DirectoryIndexSnapshot indexSnapshot = getLatestIndexSnapshot();
		if (indexSnapshot != null) {
			IndexReader indexReader = indexSnapshot.getIndexReader();
			int maxDoc = indexReader.maxDoc();
			int deletedDocs = maxDoc - indexReader.numDocs();
			if (deletedDocs > (maxDoc * percent))
				return true;
		}
		return false;
	}

	public class DiskIndexSnapshot extends DirectoryIndexSnapshot {
		private Collection<String> indexReaderFileNames;

		public DiskIndexSnapshot(Long snapshotID, IndexReader indexReader, Collection<String> indexReaderFileNames, IndexSchema.Category schemaCategory) {
			super(snapshotID, indexReader, schemaCategory);
			this.indexReaderFileNames = indexReaderFileNames;
		}
    
		// TODO: handle null range
		public ValueIndexCursor getValueIndexCursor(String field, Range range) throws Exception {
			SchemaField schemaField = schemaCategory.getField(field);
			Class type = schemaField.getFieldType().getType();
			if (cache.containsKey(new FieldCacheKey(field, type))) {
				return super.getValueIndexCursor(field, range);
			} else {
				return 
			}
		}

		public Long getHighestSnapshotId() throws IOException {
			return indexInfo.getHighestSnapshotID();
		}

		public Long getHighestDocumentId() throws IOException {
			return indexInfo.getHighestDocumentID();
		}
    
		protected void delete() throws Exception {
			super.delete();
			deleteFiles();
		}
		
		public boolean hasRef() throws Exception {
			boolean value = getCategory().getSnapshots().contains(snapshotId);
			if (value)
				return true;
			return hasSnapshotLock();
		}

		public void deleteFiles() throws IOException {
			List<File> files = getTransactionFiles();
			for (File file : files) {
				file.delete();
			}
		}

		public List<File> getFiles() throws Exception {
			List<File> files = new ArrayList<File>();
			for (String fileName : indexReaderFileNames) {
				files.add(new File(directoryFile, fileName));
			}
			files.addAll(getTransactionFiles());
			files.addAll(getFieldIndexFiles());
			return files;
		}

		public List<File> getFieldIndexFiles() {
			List<File> files = new ArrayList<File>();
			for (File file : directoryFile.listFiles()) {
				String fileName = file.getName();
				if (fileName.endsWith(".fieldindex")) {
					files.add(file);
				}
			}
			return files;
		}

		public List<File> getTransactionFiles() {
			List<File> files = new ArrayList<File>();
			String indexReaderString = Long.toString(getIndexReaderVersion(), Character.MAX_RADIX);
			for (File file : directoryFile.listFiles()) {
				String fileName = file.getName();
				if (fileName.endsWith(".tup") && fileName.startsWith(indexReaderString)) {
					files.add(file);
				}
			}
			return files;
		}
	}
  
	protected void onCommit() throws Exception {
		deleteOldSnapshotLocks();
	}

	protected DiskIndexSnapshot createNewSnapshot(Long snapshotId, IndexReader newIndexReader, IndexSchema.Category schemaCategory) throws IOException {
		IndexCommitPoint indexCommitPoint = indexDeletionPolicy.getLastIndexCommitPoint();
		assert indexCommitPoint != null;
		Collection<String> fileNames = indexCommitPoint.getFileNames();
		DiskIndexSnapshot diskIndexSnapshot = new DiskIndexSnapshot(snapshotId, newIndexReader, fileNames, schemaCategory);
		registerSnapshot(diskIndexSnapshot);
		writeSnapshotReaderSequenceMapFile(new SnapshotReaderInfo(snapshotId, newIndexReader.getVersion()));
		return diskIndexSnapshot;
	}

	public static class SnapshotReaderInfo {
		private Long snapshotid;
		private long readerVersion;

		public SnapshotReaderInfo(Long snapshotid, long readerVersion) {
			this.snapshotid = snapshotid;
			this.readerVersion = readerVersion;
		}

		public Long getSnapshotid() {
			return snapshotid;
		}

		public long getReaderVersion() {
			return readerVersion;
		}
	}

	private SnapshotReaderInfo loadHighestSnapshotReaderInfo() throws IndexException, IOException {
		try {
			List<SnapshotReaderInfo> snapshotReaderInfos = loadSnapshotReaderInfos();
			ReflectColumns reflectColumns = new ReflectColumns();
			reflectColumns.add("snapshotid", SortReflect.method("getSnapshotid"), SorterConstants.DESCENDING_ORDER);
			Sorter<SnapshotReaderInfo> sorter = Sorter.createReflect(reflectColumns, snapshotReaderInfos, SorterConstants.NULLS_ARE_LEAST);
			snapshotReaderInfos = sorter.getReflectResults();
			if (snapshotReaderInfos.size() == 0)
				return null;
			return snapshotReaderInfos.get(0);
		} catch (IOException ioException) {
			throw ioException;
		} catch (Exception exception) {
			throw new IndexException("error sorting", exception);
		}
	}

	private SnapshotReaderInfo loadSnapshotReaderInfo(long snapshotId) throws IOException {
		for (SnapshotReaderInfo snapshotReaderInfo : loadSnapshotReaderInfos()) {
			if (snapshotReaderInfo.snapshotid == snapshotId) {
				return snapshotReaderInfo;
			}
		}
		return null;
	}

	private List<SnapshotReaderInfo> loadSnapshotReaderInfos() throws IOException {
		List<SnapshotReaderInfo> snapshotReaderInfos = new ArrayList<SnapshotReaderInfo>();
		for (File file : directoryFile.listFiles()) {
			String fileName = file.getName();
			if (fileName.endsWith(".snapshotinfo")) {
				String string = fileName.substring(0, fileName.length() - 4);
				String[] split = StringUtils.split(string, "_");
				long readerVersion = Long.parseLong(split[0], Character.MAX_RADIX);
				Long snapshotid = new Long(split[1]);
				SnapshotReaderInfo snapshotReaderInfo = new SnapshotReaderInfo(snapshotid, readerVersion);
				snapshotReaderInfos.add(snapshotReaderInfo);
			}
		}
		return snapshotReaderInfos;
	}

	private static class SnapshotLock {
		private Long snapshotId;
		private String id;
		private File file;
		private long timestamp;

		public SnapshotLock(Long snapshotId, String id, File file, long timestamp) {
			this.snapshotId = snapshotId;
			this.id = id;
			this.file = file;
			this.timestamp = timestamp;
		}
	}

	public boolean hasSnapshotLock() throws IOException {
		return hasSnapshotLock(directoryFile);
	}

	public static boolean hasSnapshotLock(File directoryFile) throws IOException {
		for (File file : directoryFile.listFiles()) {
			String name = file.getName();
			if (name.endsWith(".snapshotlock")) {
				return true;
			}
		}
		return false;
	}

	public List<SnapshotLock> loadSnapshotLocks(Long snapshotID) throws IOException {
		List<SnapshotLock> snapshotLocks = new ArrayList<SnapshotLock>();
		for (File file : directoryFile.listFiles()) {
			String name = file.getName();
			if (name.endsWith(".snapshotlock")) {
				String[] split = StringUtils.split(name, "_");
				Long fileSnapshotID = new Long(split[0]);
				String id = split[1];
				if (snapshotID.equals(fileSnapshotID)) {
					SnapshotLock snapshotLock = new SnapshotLock(fileSnapshotID, id, file, file.lastModified());
					snapshotLocks.add(snapshotLock);
				}
			}
		}
		return snapshotLocks;
	}

	private void deleteOldSnapshotLocks() throws IOException {
		for (File file : directoryFile.listFiles()) {
			String name = file.getName();
			if (name.endsWith(".snapshotlock")) {
				long duration = System.currentTimeMillis() - file.lastModified();
				if (duration > 1000 * 60 * 4) {
					file.delete();
				}
			}
		}
	}

	private void writeSnapshotLockFile(long snapshotID, String id) throws IOException {
		String name = snapshotID + "_" + id + ".snapshotlock";
		File file = new File(directoryFile, name);
		FileUtils.touch(file);
	}

	private void writeSnapshotReaderSequenceMapFile(SnapshotReaderInfo snapshotReaderInfo) throws IOException {
		String readerString = Long.toString(snapshotReaderInfo.getReaderVersion(), Character.MAX_RADIX);
		String name = readerString + "_" + snapshotReaderInfo.getSnapshotid() + ".snapshotinfo";
		File file = new File(directoryFile, name);
		FileUtils.touch(file);
	}
}
